package com.dd.cloud.gateway.config;

import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.dd.cloud.common.constants.HealthConstants;
import com.dd.cloud.common.utils.AESUtil;
import com.dd.cloud.gateway.filter.MyCachedBodyOutputMessage;
import com.dd.cloud.gateway.util.PathUtil;
import com.google.common.collect.Maps;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @Description: 不同content-type的参数解密处理
 * @Author: czg
 * @Date: 2023/12/11 16:14
 * @Version: 1.0.0
 */
@Component
@Slf4j
public class ParamsHandle {
    @Autowired
    private AesencryptProperties aesencryptProperties;

    private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();
    private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";
    private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";

    private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";


    /**
     * x-www-form-urlencoded参数 解密及还原参数
     *
     * @param exchange
     * @param chain
     * @return
     */
    public Mono<Void> xwFromBody(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());

        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(Mono::just);
        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        String client = exchange.getRequest().getHeaders().getFirst("client");

        MyCachedBodyOutputMessage outputMessage = new MyCachedBodyOutputMessage(exchange, headers);
        ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
        requestBuilder.headers(k -> k.remove("Content-length"));
        return bodyInserter.insert(outputMessage, new BodyInserterContext())
                .then(Mono.defer(() -> {
                    Flux<DataBuffer> body = outputMessage.getBody();
                    DataBufferHolder holder = new DataBufferHolder();
                    xwFormDecrypt(body, holder, outputMessage, client);
                    ServerHttpRequestDecorator decorator = newDecorator(exchange, holder.length, Flux.just(holder.dataBuffer));
                    ServerHttpResponse response = exchange.getResponse();
                    //添加响应头, AesParam为true 通知前端参数要加解密  false不加解密
                    response.beforeCommit(() -> Mono.fromRunnable(() -> {
                        HttpHeaders resHeader = exchange.getResponse().getHeaders();
                        if (resHeader.containsKey(PathUtil.ENCRYPT_HANDER)) {
                            resHeader.set(PathUtil.ENCRYPT_HANDER, aesencryptProperties.getParamEncryptAble().toString());

                        } else {
                            resHeader.add(PathUtil.ENCRYPT_HANDER, aesencryptProperties.getParamEncryptAble().toString());
                        }
                        resHeader.add("Access-Control-Expose-Headers", PathUtil.ENCRYPT_HANDER);
                    }));

                    exchange.mutate().response(response);

                    return chain.filter(exchange.mutate().request(decorator).build());
                }));
    }

    /**
     * 自定义 x-www-form-urlencoded参数 即解析加密串, 并把解析后的字符串传到后续http请求中
     */
    private void xwFormDecrypt(Flux<DataBuffer> body, DataBufferHolder holder, MyCachedBodyOutputMessage outputMessage, String client) {
        body.subscribe(dataBuffer -> {
            int len = dataBuffer.readableByteCount();
            holder.length = len;
            byte[] bytes = new byte[len];
            dataBuffer.read(bytes);
            DataBufferUtils.release(dataBuffer);
            String params = new String(bytes, StandardCharsets.UTF_8);
            //+号传输会转码, 转回来
            try {
                params = URLDecoder.decode(params, StandardCharsets.UTF_8.toString());
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            log.debug("client:{}", client);
            if (!StringUtils.isBlank(client) && (client.equals(HealthConstants.ClientType.HEALTHPATIENT) ||
                    client.equals(HealthConstants.ClientType.HEALTHAPPLET) ||
                    client.equals(HealthConstants.ClientType.HEALTHMOB))) {
                //此时是移动端, 参数会变成queryString, 要转回来
                params = queryString(params);
            }
            log.info("xw解密前的参数:{}", params);
            try {
                params = AESUtil.decrypt(params, aesencryptProperties.getKey());
            } catch (Exception e) {
                log.error("xw数据解密失败");
                e.printStackTrace();
            }
            if (params == null) {
                params = "";
            }
            log.info("xw解密后的参数:{}", params);
            DataBuffer data = outputMessage.bufferFactory().allocateBuffer();
            data.write(params.getBytes(StandardCharsets.UTF_8));
            holder.length = data.readableByteCount();
            holder.dataBuffer = data;
        });
    }

    private ServerHttpRequestDecorator newDecorator(ServerWebExchange exchange, long dataLength, Flux<DataBuffer> body) {
        return new ServerHttpRequestDecorator(
                exchange.getRequest()) {

            /**
             * 有需要这里可以重写request的headers
             */
            @Override
            public HttpHeaders getHeaders() {
                return super.getHeaders();
            }

            @Override
            public Flux<DataBuffer> getBody() {
                return body;
            }
        };
    }

    /**
     * json参数解密, 并把解密串设置到body
     */
    public Mono<Void> jsonDecrypt(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerRequest serverRequest = ServerRequest.create(exchange, HandlerStrategies.withDefaults().messageReaders());

        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(Mono::just);
        String client = exchange.getRequest().getHeaders().getFirst("client");

        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());
        headers.remove(HttpHeaders.CONTENT_LENGTH);
        MyCachedBodyOutputMessage outputMessage = new MyCachedBodyOutputMessage(exchange, headers);
        ServerHttpRequest.Builder requestBuilder = exchange.getRequest().mutate();
        requestBuilder.headers(k -> k.remove("Content-length"));

        Mono mono = bodyInserter.insert(outputMessage, new BodyInserterContext())
                .then(Mono.defer(() -> {
                    //解决body内数据过长读取不完整的问题
                    Flux<DataBuffer> body = outputMessage.getBody();
                    DataBufferHolder holder = new DataBufferHolder();
                    try {
                        body.subscribe(dataBuffer -> {
                            int len = dataBuffer.readableByteCount();
                            holder.length = len;
                            byte[] bytes = new byte[len];
                            dataBuffer.read(bytes);
                            DataBufferUtils.release(dataBuffer);
                            String params = new String(bytes, StandardCharsets.UTF_8);
                            log.debug("client:{}", client);

                            if (!StringUtils.isBlank(client) && (client.equals(HealthConstants.ClientType.HEALTHPATIENT) ||
                                    client.equals(HealthConstants.ClientType.HEALTHAPPLET) ||
                                    client.equals(HealthConstants.ClientType.HEALTHMOB))) {
                                //此时是移动端, 参数会变成queryString, 要转回来
                                params = jsonString(params);
                            }
                            log.info("json解密前的参数,{}", params);
                            try {
                                params = AESUtil.decrypt(params, aesencryptProperties.getKey());
                            } catch (Exception e) {
                                log.error("数据解密失败");
                                e.printStackTrace();
                            }
                            if (params == null) {
                                params = "";
                            }
                            log.info("json解密后的参数,{}", params);
                            DataBuffer data = outputMessage.bufferFactory().allocateBuffer();
                            data.write(params.toString().getBytes(StandardCharsets.UTF_8));
                            holder.length = data.readableByteCount();
                            holder.dataBuffer = data;
                        });

                    } catch (Exception e) {
                        e.printStackTrace();
                    }

                    ServerHttpRequestDecorator decorator = newDecorator(exchange, holder.length, Flux.just(holder.dataBuffer));
                    //修改响应体
                    ServerHttpResponse response = exchange.getResponse();
                    //添加响应头, AesParam为true 通知前端参数要加解密  false不加解密  AesResutl范围值true表示结果加密 false不加密说
                    response.beforeCommit(() -> Mono.fromRunnable(() -> {
                        HttpHeaders resHeader = exchange.getResponse().getHeaders();

                        if (resHeader.containsKey(PathUtil.ENCRYPT_HANDER)) {
                            resHeader.set(PathUtil.ENCRYPT_HANDER, aesencryptProperties.getParamEncryptAble().toString());

                        } else {
                            resHeader.add(PathUtil.ENCRYPT_HANDER, aesencryptProperties.getParamEncryptAble().toString());
                        }
                        //加这个前端才能取到response的header
                        resHeader.add("Access-Control-Expose-Headers", PathUtil.ENCRYPT_HANDER);
                    }));

                    exchange.mutate().response(response);


                    return chain.filter(exchange.mutate().request(decorator).build());

                }));

        return mono;
    }


    /**
     * @param contentType:
     * @param exchange:
     * @param chain:
     * @return Mono<Void>
     * @description 修改form参数
     * @author czg
     * @date 2023/12/11 16:25
     */
    public Mono<Void> formDecrypt(MediaType contentType, ServerWebExchange exchange, GatewayFilterChain chain) {
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .flatMap(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    String oldBody = addPara(contentType.toString(), new String(bytes));

                    byte[] bytes1 = oldBody.getBytes();
                    byte[] bytes2 = byteMerger(Arrays.copyOf(bytes, bytes.length - 4), bytes1);
                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes2);
                        DataBufferUtils.retain(buffer);
                        return Mono.just(buffer);
                    });
                    ServerHttpRequestDecorator mutatedRequest = newDecorator(exchange, bytes2.length, cachedFlux);
                    ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
                    return ServerRequest.create(mutatedExchange, MESSAGE_READERS)
                            .bodyToMono(byte[].class)
                            .then(chain.filter(mutatedExchange));
                });
    }

    /**
     * 修改form参数
     *
     * @param contentType 请求类型
     * @param bodyString  请求body信息
     */
    @SneakyThrows
    private static String addPara(String contentType, String bodyString) {
        StringBuffer stringBuffer = new StringBuffer();

        String boundary = contentType.substring(contentType.lastIndexOf("boundary=") + 9);//获取随机字符传信息
        String boundary_end = StrUtil.format("--{}--\r\n", boundary);
        Map<String, Object> formMap = Maps.newHashMap();
        /**
         *
         * 根据自己需求进行对bodyString信息修改，例如下面，根据boundary对传入的bodyString进行了分割
         *  String[] split = bodyString.split(boundary);
         *  然后将修改完后的信息封装到formMap中，需要注意的是，file文件需要以new FileResource(file, fileName)的形式当作value放到formMap中
         */
        String part = "^\r\nContent-Disposition: form-data; name=\"([^/?]+)\"\r\n\r\n([^/?]+)\r\n--?$";
        Pattern r = Pattern.compile(part);
        String[] split = bodyString.split(boundary);
        for (int x = 1; x < split.length - 1; x++) {
            Matcher m = r.matcher(split[x]);
            if (m.find()) {
                String name = m.group(1);
                String value = m.group(2);
                System.out.println("name:" + name + " value:" + value);
//				formMap.put(name,value);
            }
        }

        formMap.put("ft", 11111);
        formMap.put("tcu", 22222);
        Integer count = 0;
        for (Map.Entry<String, Object> entry : formMap.entrySet()) {
            stringBuffer.append(appendPart(boundary, entry.getKey(), entry.getValue(), count));
            count++;
        }
        stringBuffer.append(boundary_end);//拼接结束信息
        log.info(stringBuffer.toString());

        return stringBuffer.toString();
    }

    /**
     * 添加Multipart表单的数据项
     *
     * @param boundary      随机串信息
     * @param formFieldName 表单名
     * @param value         值，可以是普通值、资源（如文件等）
     */
    private static String appendPart(String boundary, String formFieldName, Object value, Integer count) throws IORuntimeException {
        StringBuffer stringBuffer = new StringBuffer();
        // 多资源
        if (value instanceof MultiResource) {
            for (Resource subResource : (MultiResource) value) {
                appendPart(boundary, formFieldName, subResource, count);
            }
            return stringBuffer.toString();
        }

        if (count != 0) {
            stringBuffer.append("--").append(boundary).append(StrUtil.CRLF);
        } else {
            stringBuffer.append(StrUtil.CRLF);
//			stringBuffer.append(boundary).append(StrUtil.CRLF);
        }

        if (value instanceof Resource) {
            // 文件资源（二进制资源）
            final Resource resource = (Resource) value;
            final String fileName = resource.getName();
            stringBuffer.append(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, ObjectUtil.defaultIfNull(fileName, formFieldName)));
            // 根据name的扩展名指定互联网媒体类型，默认二进制流数据
            stringBuffer.append(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName, "application/octet-stream")));
        } else {
            // 普通数据
            stringBuffer.append(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName)).append(value);
        }
        stringBuffer.append(StrUtil.CRLF);
        return stringBuffer.toString();
    }


    private static byte[] byteMerger(byte[] bt1, byte[] bt2) {
        byte[] bt3 = new byte[bt1.length + bt2.length];
        System.arraycopy(bt1, 0, bt3, 0, bt1.length);
        System.arraycopy(bt2, 0, bt3, bt1.length, bt2.length);
        return bt3;
    }

    private static String queryString(String queryString) {
        if (StringUtils.isBlank(queryString) || queryString.indexOf("&") < 0) {
            return queryString;
        }
        String[] params = queryString.split("&");
        StringBuilder sb = new StringBuilder();
        for (String param : params) {
            String substring = param.substring(param.indexOf("=") + 1);
            sb.append(substring);
        }
        return sb.toString();
    }

    public static String jsonString(String jsonString) {
        JSONObject jsonObject = JSONObject.parseObject(jsonString, Feature.OrderedField);
        System.out.println(jsonObject);
        Set<String> keys = jsonObject.keySet();
        StringBuilder sb = new StringBuilder();
        keys.forEach(item -> {
            String string = jsonObject.getString(item);
            sb.append(string);
        });
        return sb.toString();
    }

    private class DataBufferHolder {
        DataBuffer dataBuffer;
        int length;
    }
}
